# -*- coding: utf-8 -*-
# @Time    : 2024/7/16 16:00
# @Author  : yujiahao
# @File    : 34_fastapi_safety_hash_jwt.py
# @description:OAuth2 实现密码哈希与 Bearer JWT 令牌验证


"""
OAuth2 实现密码哈希与 Bearer JWT 令牌验证：
------------------------------------------------
1. 至此，我们已经编写了所有安全流，这里看一下如何使用 JWT 令牌（Token）和安全密码哈希（Hash）实现真正的安全机制。
2. 本章的示例代码真正实现了在应用的数据库中保存哈希密码等功能。
3. 接下来，我们紧接上一章，继续完善安全机制。

JWT 简介：
----------
1. JWT 即 JSON 网络令牌（JSON Web Tokens）。
2. JWT 是一种将 JSON 对象编码为没有空格，且难以理解的长字符串的标准。JWT 的内容如下所示：
   eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
3. JWT 字符串没有加密，任何人都能用它恢复原始信息。
4. 但 JWT 使用了签名机制。接受令牌时，可以用签名校验令牌。
5. 使用 JWT 创建有效期为一周的令牌。第二天，用户持令牌再次访问时，仍为登录状态。
6. 令牌于一周后过期，届时，用户身份验证就会失败。只有再次登录，才能获得新的令牌。
7. 如果用户（或第三方）篡改令牌的过期时间，因为签名不匹配会导致身份验证失败。
8. 如需深入了解 JWT 令牌，了解它的工作方式，请参阅 https://jwt.io。
安装 PyJWT，在 Python 中生成和校验 JWT 令牌：pip install pyjwt


密码哈希：
-----------
1. 哈希是指把特定内容（本例中为密码）转换为乱码形式的字节序列（其实就是字符串）。
2. 每次传入完全相同的内容时（比如，完全相同的密码），返回的都是完全相同的乱码。
3. 但这个乱码无法转换回传入的密码。

为什么使用密码哈希：
------------------------
1. 原因很简单，假如数据库被盗，窃贼无法获取用户的明文密码，得到的只是哈希值。
2. 这样一来，窃贼就无法在其它应用中使用窃取的密码（要知道，很多用户在所有系统中都使用相同的密码，风险超大）。

安装 passlib：
---------------
1. Passlib 是处理密码哈希的 Python 包。
2. 它支持很多安全哈希算法及配套工具。
3. 本教程推荐的算法是 Bcrypt。
4. 因此，请先安装附带 Bcrypt 的 PassLib：
   pip install passlib[bcrypt]

提示：
------
1. passlib 甚至可以读取 Django、Flask 的安全插件等工具创建的密码。
2. 例如，把 Django 应用的数据共享给 FastAPI 应用的数据库。或利用同一个数据库，可以逐步把应用从 Django 迁移到 FastAPI。
3. 并且，用户可以同时从 Django 应用或 FastAPI 应用登录。
"""

"""
简介：
------
这个代码示例展示了如何使用 FastAPI 和 JWT 实现一个简单的用户认证和授权系统。
通过使用 PassLib 进行密码哈希和验证，确保密码的安全存储和校验。
通过 JWT 生成和验证访问令牌，实现用户身份的安全认证和授权。

主要功能：
-----------
1. 用户登录：用户提交用户名和密码，系统验证后生成 JWT 访问令牌。
2. 获取用户信息：通过验证 JWT 令牌，获取当前用户的信息。
3. 获取用户项目：通过验证 JWT 令牌，获取当前用户的项目列表。

主要步骤：
-----------
1. 导入所需模块和库。
2. 定义 Pydantic 模型，用于请求和响应的数据结构。
3. 创建密码哈希上下文，使用 bcrypt 算法。
4. 定义辅助函数，用于密码验证、用户身份验证和 JWT 令牌生成。
5. 创建 FastAPI 应用，定义 API 端点处理用户登录和信息获取。

密码哈希与校验：
创建了三个工具函数：
    第一个函数用于校验接收的密码是否匹配存储的哈希值。
    第二个函数用于哈希用户的密码。
    第三个函数用于身份验证，并返回用户。
"""

from datetime import datetime, timedelta, timezone
from typing import Union, Annotated
from pydantic import BaseModel
from fastapi import Depends, FastAPI, HTTPException, status

#  todo JWT 0、导入依赖
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

# todo 更新依赖项 0、导入依赖
import jwt
from jwt.exceptions import InvalidTokenError

# todo 密码哈希与校验 0、导入依赖
from passlib.context import CryptContext

# todo JWT 1、处理 JWT 令牌（秘钥、签名算法、过期时间）

# 生成一个随机的密钥，用于JWT签名
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"  # JWT签名算法
ACCESS_TOKEN_EXPIRE_MINUTES = 30  # 访问令牌过期时间

'''
一、随机秘钥
    生成一个随机的密钥，用于JWT签名
    使用命令: openssl rand -hex 32


二、JWT签名算法
    创建指定 JWT 令牌签名算法的变量 ALGORITHM 是为了明确指定在生成和验证 JWT（JSON Web Token）时所使用的签名算法。
    JWT 使用签名算法来确保令牌的完整性和真实性，即确保令牌在传输过程中没有被篡改，并且确实是由可信的发行者生成的。
    
    详细解释
    
    什么是 JWT 签名算法？
    JWT 令牌通常由三部分组成：头部（header）、载荷（payload）和签名（signature）。
    头部包含令牌类型和签名算法，载荷包含声明（claims），签名则用于验证令牌的真实性和完整性。
    
    签名算法是用来生成和验证签名的算法。常见的签名算法包括：
    
        - HS256：HMAC 使用 SHA-256 哈希算法
        - RS256：RSA 使用 SHA-256 哈希算法
        - ES256：ECDSA 使用 P-256 和 SHA-256
    为什么需要指定签名算法？
        指定签名算法的主要原因是为了确保生成和验证 JWT 时使用相同的算法。
        如果生成和验证 JWT 时使用了不同的算法，验证过程将失败，令牌将被认为是无效的。

三、访问令牌过期时间
'''

# 模拟数据库，包含用户信息和哈希后的密码
fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}


# todo JWT 2、Pydantic模型，用于定义Token的响应结构
class Token(BaseModel):
    access_token: str
    token_type: str


# Pydantic模型，用于从JWT中提取的用户数据
class TokenData(BaseModel):
    username: Union[str, None] = None


# 用户基本信息模型
class User(BaseModel):
    username: str
    email: Union[str, None] = None
    full_name: Union[str, None] = None
    disabled: Union[bool, None] = None


# 包含哈希密码的用户信息模型，继承自User
class UserInDB(User):
    hashed_password: str


# todo 密码哈希与校验 1、 创建密码哈希上下文，使用bcrypt算法
'''
创建一个 CryptContext 对象，该对象是 passlib 库的一部分，用于处理密码哈希和验证。

详细解释
1、CryptContext：

    - CryptContext 是 passlib 库提供的一个类，用于管理密码哈希算法和策略。它可以帮助你轻松地哈希密码、验证密码，并在需要时更新密码哈希。
    
2、schemes=["bcrypt"]：

    - schemes 参数指定了要使用的哈希算法列表。在这个例子中，指定了 bcrypt 算法。
    - bcrypt 是一种流行的密码哈希算法，因其安全性和适度的计算成本而被广泛使用。
    
3、deprecated="auto"：

    - deprecated 参数用于指定哪些哈希算法已被弃用。当设置为 "auto" 时，passlib 会自动管理哈希算法的弃用状态。
    - 这意味着，如果你在未来需要更改哈希算法（例如，从 bcrypt 切换到 argon2），passlib 会自动识别旧的哈希算法并继续验证旧的哈希值，同时使用新的哈希算法生成新的哈希值。
'''
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# OAuth2PasswordBearer对象，用于获取token
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

app = FastAPI()

# todo 密码哈希与校验 2、验证密码是否匹配
# 参数
# plain_password：用户输入的明文密码。
# hashed_password：存储在数据库中的哈希密码。
# 返回值
# True：如果明文密码与哈希密码匹配。
# False：如果明文密码与哈希密码不匹配。
# 这里注意要额外按照一个库：pip install bcrypt，否则会报错，原因如下：
"""
passlib 需要一个具体的后端来执行哈希算法。在你的情况中，bcrypt 是被选择的后端。
然而，默认情况下，bcrypt 并不会自动安装在你的环境中。所以当 passlib 试图使用 bcrypt 进行哈希处理时，
如果没有安装 bcrypt 库，就会出现 MissingBackendError。
"""


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


# todo 密码哈希与校验 3、获取密码的哈希值
def get_password_hash(password):
    return pwd_context.hash(password)


# 从数据库中获取用户信息
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 验证用户身份，检查用户名和密码是否匹配
def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


# todo JWT 3、 创建访问令牌(创建生成新的访问令牌的工具函数。)
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
    '''
    参数
    data: dict：包含要编码到 JWT 中的数据（通常是用户信息或声明）。
    expires_delta: Union[timedelta, None] = None：一个可选的 timedelta 对象，指定令牌的有效期。如果未提供，则默认有效期为 15 分钟。
    返回一个jwt
    '''

    # copy 一个副本，防止修改原始数据
    to_encode = data.copy()
    # 设置令牌的过期时间,设置令牌的过期时间，如果有就在当前时间加 expires_delta 时间，如果没有就指定15分钟
    if expires_delta:
        expire = datetime.now(timezone.utc) + expires_delta
    else:
        expire = datetime.now(timezone.utc) + timedelta(minutes=15)
    # 将过期时间增加到字典中
    to_encode.update({"exp": expire})
    # 使用 jwt.encode 方法生成一个 JWT。它使用 to_encode 作为载荷，SECRET_KEY 作为签名密钥，ALGORITHM 作为签名算法。
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


# todo 更新依赖项 1、获取当前用户信息，验证token的有效性
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    '''
    更新 get_current_user 以接收与之前相同的令牌，但这里用的是 JWT 令牌。
    解码并校验接收到的令牌，然后，返回当前用户。
    如果令牌无效，则直接返回 HTTP 错误。

    参数：
        token: Annotated[str, Depends(oauth2_scheme)]：这是一个依赖项，表示该函数依赖于 oauth2_scheme 提供的 OAuth2 令牌。
        oauth2_scheme 是一个 FastAPI 的 OAuth2PasswordBearer 实例，它会从请求中提取 Bearer 令牌。
    '''

    # 1、定义认证异常：
    # 定义了一个 HTTPException 对象 credentials_exception，用于在令牌无效时抛出异常。它设置了状态码 401 和相关的错误信息。
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    # 2、解码 JWT 令牌：
    '''
        - 使用 jwt.decode 方法解码 token，并使用 SECRET_KEY 和 ALGORITHM 验证其签名。
        - 从解码后的 payload 中获取 username。
        - 如果 username 不存在，抛出 credentials_exception。
        - 将 username 封装到 TokenData 对象中。
    
    JWT 规范定义了一些标准的声明（claims），包括但不限于：

        sub（Subject）：
        
            - 如果你需要唯一标识令牌的持有者，例如用户 ID，使用 sub 是一个常见的选择。
            - 示例：在用户登录后生成的令牌中，sub 可以是用户的唯一标识符。
        aud（Audience）：
        
            - 如果你需要确保令牌只能被特定的服务或应用使用，使用 aud 来指定受众。
            - 示例：如果你的应用有多个服务，aud 可以用来确保令牌只能被特定的服务接受。
        iss（Issuer）：
        
            -如果你需要验证令牌的来源，使用 iss 来指定令牌的签发者。
            -示例：在多租户系统中，iss 可以用来区分不同租户的令牌。
        exp（Expiration Time）：
        
            - 如果你需要限制令牌的有效期，使用 exp 来指定令牌的过期时间。
            - 示例：短期有效的访问令牌通常会设置 exp 来确保令牌在一定时间后失效。
        nbf（Not Before） 和 iat（Issued At）：
        
            -   如果你需要控制令牌的生效时间，使用 nbf 和 iat。
            -   示例：nbf 可以确保令牌在未来某个时间点之前不可用，而 iat 可以记录令牌的签发时间。
    '''
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except InvalidTokenError:
        raise credentials_exception

    # 3、获取用户信息：
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    return user


# 获取当前激活的用户，验证用户是否被禁用
async def get_current_active_user(current_user: Annotated[User, Depends(get_current_user)]):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


# todo 更新 /token 路径操作 1、 登录端点，处理用户登录并生成访问令牌（在UI界面授权确实会进行一次POST的操作，如果注释掉会报错）
@app.post("/token")
async def login_for_access_token(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
    # form_data: Annotated[OAuth2PasswordRequestForm, Depends()]：
    # 使用 OAuth2PasswordRequestForm 作为函数参数，该参数通过依赖注入（Depends）获取。
    # OAuth2PasswordRequestForm 是 FastAPI 提供的一个类，用于处理 OAuth2 密码模式的表单数据，包括 username 和 password。

    # 1、用户认证
    # authenticate_user：这是一个自定义函数，用于验证用户凭据（用户名和密码）。fake_users_db 是一个模拟的用户数据库。
    # form_data.username 和 form_data.password：从表单数据中提取的用户名和密码。
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)

    # 2、验证失败处理
    """
        - if not user:：如果用户验证失败（authenticate_user 返回 None 或 False），则抛出 HTTP 401 未授权异常。
        - HTTPException：FastAPI 提供的异常类，用于返回特定的 HTTP 状态码和错误信息。
        - status_code=status.HTTP_401_UNAUTHORIZED：设置 HTTP 状态码为 401（未授权）。
        - detail="Incorrect username or password"：错误详情信息，返回给客户端。
        - headers={"WWW-Authenticate": "Bearer"}：设置 WWW-Authenticate 头，指示客户端使用 Bearer 令牌进行认证。
    """
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )

    # 3、生成访问令牌
    '''
        - access_token_expires：定义访问令牌的过期时间，这里使用 timedelta 设置为若干分钟（ACCESS_TOKEN_EXPIRE_MINUTES 是预先定义的常量）。
        - create_access_token：自定义函数，用于创建 JWT 访问令牌。
        - data={"sub": user.username}：在生成令牌时包含用户的用户名作为 sub（subject）声明，用于标识令牌的持有者。
        - expires_delta=access_token_expires：设置令牌的过期时间。
    '''
    access_token_expires = timedelta(seconds=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    # 4、返回令牌
    """
        - Token：这是一个 Pydantic 模型，用于定义返回的令牌结构。
        - access_token=access_token：返回生成的访问令牌。
        - token_type="bearer"：指定令牌类型为 bearer，这是一种常见的认证令牌类型。
    """
    return Token(access_token=access_token, token_type="bearer")


# 获取当前用户信息
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: Annotated[User, Depends(get_current_active_user)]):
    return current_user


# 获取当前用户的项目
@app.get("/users/me/items/")
async def read_own_items(current_user: Annotated[User, Depends(get_current_active_user)]):
    return [{"item_id": "Foo", "owner": current_user.username}]


"""
JWT sub 的技术细节：
----------------------
1. JWT 规范还包括 sub 键，值是令牌的主题。
2. 该键是可选的，但要把用户标识放在这个键里，所以本例使用了该键。
3. 除了识别用户与许可用户在 API 上直接执行操作之外，JWT 还可能用于其它事情。
   例如，识别汽车或博客。
4. 接着，为实体添加权限，比如驾驶（汽车）或编辑（博客）。
5. 然后，把 JWT 令牌交给用户（或机器人），他们就可以执行驾驶汽车，或编辑博客等操作。无需注册账户，只要有 API 生成的 JWT 令牌就可以。
6. 同理，JWT 可以用于更复杂的场景。
7. 在这些情况下，多个实体的 ID 可能是相同的，以 ID foo 为例，用户的 ID 是 foo，车的 ID 是 foo，博客的 ID 也是 foo。
8. 为了避免 ID 冲突，在给用户创建 JWT 令牌时，可以为 sub 键的值加上前缀，例如 username:。因此，在本例中，sub 的值可以是：username:johndoe。
9. 注意，划重点，sub 键在整个应用中应该只有一个唯一的标识符，而且应该是字符串。

检查：
------
1. 运行服务器并访问文档： http://127.0.0.1:8000/docs。
2. 可以看到如下用户界面：
   - 用与上一章同样的方式实现应用授权。
   - 使用如下凭证：
     用户名: johndoe
     密码: secret
3. 注意，代码中没有明文密码 secret，只保存了它的哈希值。
4. 调用 /users/me/ 端点，收到下面的响应：
   {
     "username": "johndoe",
     "email": "johndoe@example.com",
     "full_name": "John Doe",
     "disabled": false
   }
5. 打开浏览器的开发者工具，查看数据是怎么发送的，而且数据里只包含了令牌，只有验证用户的第一个请求才发送密码，并获取访问令牌，但之后不会再发送密码。
6. 注意，请求中 Authorization 响应头的值以 Bearer 开头。

scopes 高级用法：
-----------------
1. OAuth2 支持 scopes（作用域）。
2. scopes 为 JWT 令牌添加指定权限。
3. 让持有令牌的用户或第三方在指定限制条件下与 API 交互。
4. 高级用户指南中将介绍如何使用 scopes，及如何把 scopes 集成至 FastAPI。

小结：
------
1. 至此，您可以使用 OAuth2 和 JWT 等标准配置安全的 FastAPI 应用。
2. 几乎在所有框架中，处理安全问题很快都会变得非常复杂。
3. 有些包为了简化安全流，不得不在数据模型、数据库和功能上做出妥协。而有些过于简化的软件包其实存在了安全隐患。
4. FastAPI 不向任何数据库、数据模型或工具做妥协。
5. 开发者可以灵活选择最适合项目的安全机制。
6. 还可以直接使用 passlib 和 PyJWT 等维护良好、使用广泛的包，这是因为 FastAPI 不需要任何复杂机制，就能集成外部的包。
7. 而且，FastAPI 还提供了一些工具，在不影响灵活、稳定和安全的前提下，尽可能地简化安全机制。
8. FastAPI 还支持以相对简单的方式，使用 OAuth2 等安全、标准的协议。
9. 高级用户指南中详细介绍了 OAuth2 scopes 的内容，遵循同样的标准，实现更精密的权限系统。OAuth2 的作用域是脸书、谷歌、GitHub、微软、推特等第三方身份验证应用使用的机制，让用户授权第三方应用与 API 交互。
"""
